﻿using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
#if !NET40
using System.Runtime.CompilerServices;
#endif

namespace System.Threading.Tasks
{
  internal static class CaTaskExtensions
  {
    private static readonly Task<object> CanceledTask = TaskFromCanceled<object>();
#if NET40
    private static readonly Task<object> CompletedTask = TaskEx.FromResult(default(object));
#else
    private static readonly Task<object> CompletedTask = Task.FromResult(default(object));
#endif

    private static readonly Action<Task> IgnoreTaskContinuation = t => { var ignored = t.Exception; };

    /// <summary>Observes and ignores a potential exception on a given Task.
    /// If a Task fails and throws an exception which is never observed, it will be caught by the .NET finalizer thread.
    /// This function awaits the given task and if the exception is thrown, it observes this exception and simply ignores it.
    /// This will prevent the escalation of this exception to the .NET finalizer thread.</summary>
    /// <param name="task">The task to be ignored.</param>
    [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "ignored")]
    internal static void Ignore(this Task task)
    {
      if (task.IsCompleted)
      {
        var ignored = task.Exception;
      }
      else
      {
        task.ContinueWith(
            IgnoreTaskContinuation,
            CancellationToken.None,
            TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
      }
    }

    /// <summary>
    /// Returns a <see cref="Task{Object}"/> for the provided <see cref="Task"/>.
    /// </summary>
    /// <param name="task">The task.</param>
    public static Task<object> ToUntypedTask(this Task task)
    {
      switch (task.Status)
      {
        case TaskStatus.RanToCompletion:
          return CompletedTask;

        case TaskStatus.Faulted:
          return TaskFromFaulted(task);

        case TaskStatus.Canceled:
          return CanceledTask;

        default:
          return ConvertAsync(task);
      }

      async Task<object> ConvertAsync(Task asyncTask)
      {
        await asyncTask;
        return null;
      }
    }

    /// <summary>
    /// Returns a <see cref="Task{Object}"/> for the provided <see cref="Task{T}"/>.
    /// </summary>
    /// <typeparam name="T">The underlying type of <paramref name="task"/>.</typeparam>
    /// <param name="task">The task.</param>
    public static Task<object> ToUntypedTask<T>(this Task<T> task)
    {
      if (typeof(T) == typeof(object))
        return task as Task<object>;

      switch (task.Status)
      {
        case TaskStatus.RanToCompletion:
#if NET40
          return TaskEx.FromResult((object)GetResult(task));
#else
          return Task.FromResult((object)GetResult(task));
#endif

        case TaskStatus.Faulted:
          return TaskFromFaulted(task);

        case TaskStatus.Canceled:
          return CanceledTask;

        default:
          return ConvertAsync(task);
      }

      async Task<object> ConvertAsync(Task<T> asyncTask)
      {
        return await asyncTask;
      }
    }

    /// <summary>
    /// Returns a <see cref="Task{Object}"/> for the provided <see cref="Task{T}"/>.
    /// </summary>
    /// <typeparam name="T">The underlying type of <paramref name="task"/>.</typeparam>
    /// <param name="task">The task.</param>
    internal static Task<T> ToTypedTask<T>(this Task<object> task)
    {
      if (typeof(T) == typeof(object))
        return task as Task<T>;

      switch (task.Status)
      {
        case TaskStatus.RanToCompletion:
#if NET40
          return TaskEx.FromResult((T)GetResult(task));
#else
          return Task.FromResult((T)GetResult(task));
#endif

        case TaskStatus.Faulted:
          return TaskFromFaulted<T>(task);

        case TaskStatus.Canceled:
          return TaskFromCanceled<T>();

        default:
          return ConvertAsync(task);
      }

      async Task<T> ConvertAsync(Task<object> asyncTask)
      {
        return (T)await asyncTask;
      }
    }

    /// <summary>
    /// Returns a <see cref="Task{Object}"/> for the provided <see cref="Task{Object}"/>.
    /// </summary>
    /// <param name="task">The task.</param>
    public static Task<object> ToUntypedTask(this Task<object> task)
    {
      return task;
    }

    private static Task<object> TaskFromFaulted(Task task)
    {
      var completion = new TaskCompletionSource<object>();
      completion.SetException(task.Exception.InnerExceptions);
      return completion.Task;
    }

    private static Task<T> TaskFromFaulted<T>(Task task)
    {
      var completion = new TaskCompletionSource<T>();
      completion.SetException(task.Exception.InnerExceptions);
      return completion.Task;
    }

    private static Task<T> TaskFromCanceled<T>()
    {
      var completion = new TaskCompletionSource<T>();
      completion.SetCanceled();
      return completion.Task;
    }

    public static async Task LogException(this Task task, ILogger logger, string message)
    {
      try
      {
        await task;
      }
      catch (Exception exc)
      {
        var ignored = task.Exception; // Observe exception
        logger.LogError(exc, message);
        throw;
      }
    }

    // Executes an async function such as Exception is never thrown but rather always returned as a broken task.
    public static async Task SafeExecute(Func<Task> action)
    {
      await action();
    }

    public static async Task ExecuteAndIgnoreException(Func<Task> action)
    {
      try
      {
        await action();
      }
      catch (Exception)
      {
        // dont re-throw, just eat it.
      }
    }

    internal static String ToString(this Task t)
    {
      return t == null ? "null" : string.Format("[Id={0}, Status={1}]", t.Id, Enum.GetName(typeof(TaskStatus), t.Status));
    }

    internal static String ToString<T>(this Task<T> t)
    {
      return t == null ? "null" : string.Format("[Id={0}, Status={1}]", t.Id, Enum.GetName(typeof(TaskStatus), t.Status));
    }


    internal static void WaitWithThrow(this Task task, TimeSpan timeout)
    {
      if (!task.Wait(timeout))
      {
        throw new TimeoutException($"Task.WaitWithThrow has timed out after {timeout}.");
      }
    }

    internal static T WaitForResultWithThrow<T>(this Task<T> task, TimeSpan timeout)
    {
      if (!task.Wait(timeout))
      {
        throw new TimeoutException($"Task<T>.WaitForResultWithThrow has timed out after {timeout}.");
      }
      return task.Result;
    }

    /// <summary>
    /// This will apply a timeout delay to the task, allowing us to exit early
    /// </summary>
    /// <param name="taskToComplete">The task we will timeout after timeSpan</param>
    /// <param name="timeout">Amount of time to wait before timing out</param>
    /// <param name="exceptionMessage">Text to put into the timeout exception message</param>
    /// <exception cref="TimeoutException">If we time out we will get this exception</exception>
    /// <returns>The completed task</returns>
    internal static async Task WithTimeout(this Task taskToComplete, TimeSpan timeout, string exceptionMessage = null)
    {
      if (taskToComplete.IsCompleted)
      {
        await taskToComplete;
        return;
      }

      var timeoutCancellationTokenSource = new CancellationTokenSource();
#if NET40
      var completedTask = await TaskEx.WhenAny(taskToComplete, TaskEx.Delay(timeout, timeoutCancellationTokenSource.Token));
#else
      var completedTask = await Task.WhenAny(taskToComplete, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
#endif

      // We got done before the timeout, or were able to complete before this code ran, return the result
      if (taskToComplete == completedTask)
      {
        timeoutCancellationTokenSource.Cancel();
        // Await this so as to propagate the exception correctly
        await taskToComplete;
        return;
      }

      // We did not complete before the timeout, we fire and forget to ensure we observe any exceptions that may occur
      taskToComplete.Ignore();
      var errorMessage = exceptionMessage ?? $"WithTimeout has timed out after {timeout}";
      throw new TimeoutException(errorMessage);
    }

    /// <summary>
    /// This will apply a timeout delay to the task, allowing us to exit early
    /// </summary>
    /// <param name="taskToComplete">The task we will timeout after timeSpan</param>
    /// <param name="timeSpan">Amount of time to wait before timing out</param>
    /// <param name="exceptionMessage">Text to put into the timeout exception message</param>
    /// <exception cref="TimeoutException">If we time out we will get this exception</exception>
    /// <returns>The value of the completed task</returns>
    public static async Task<T> WithTimeout<T>(this Task<T> taskToComplete, TimeSpan timeSpan, string exceptionMessage = null)
    {
      if (taskToComplete.IsCompleted)
      {
        return await taskToComplete;
      }

      var timeoutCancellationTokenSource = new CancellationTokenSource();
#if NET40
      var completedTask = await TaskEx.WhenAny(taskToComplete, TaskEx.Delay(timeSpan, timeoutCancellationTokenSource.Token));
#else
      var completedTask = await Task.WhenAny(taskToComplete, Task.Delay(timeSpan, timeoutCancellationTokenSource.Token));
#endif

      // We got done before the timeout, or were able to complete before this code ran, return the result
      if (taskToComplete == completedTask)
      {
        timeoutCancellationTokenSource.Cancel();
        // Await this so as to propagate the exception correctly
        return await taskToComplete;
      }

      // We did not complete before the timeout, we fire and forget to ensure we observe any exceptions that may occur
      taskToComplete.Ignore();
      var errorMessage = exceptionMessage ?? $"WithTimeout has timed out after {timeSpan}";
      throw new TimeoutException(errorMessage);
    }

    /// <summary>For making an uncancellable task cancellable, by ignoring its result.</summary>
    /// <param name="taskToComplete">The task to wait for unless cancelled</param>
    /// <param name="cancellationToken">A cancellation token for cancelling the wait</param>
    /// <param name="message">Message to set in the exception</param>
    /// <returns></returns>
    internal static async Task WithCancellation(this Task taskToComplete, CancellationToken cancellationToken, string message)
    {
      try
      {
        await taskToComplete.WithCancellation(cancellationToken);
      }
      catch (TaskCanceledException ex)
      {
        throw new TaskCanceledException(message, ex);
      }
    }

    /// <summary>For making an uncancellable task cancellable, by ignoring its result.</summary>
    /// <param name="taskToComplete">The task to wait for unless cancelled</param>
    /// <param name="cancellationToken">A cancellation token for cancelling the wait</param>
    /// <returns></returns>
    internal static Task WithCancellation(this Task taskToComplete, CancellationToken cancellationToken)
    {
      if (taskToComplete.IsCompleted || !cancellationToken.CanBeCanceled)
      {
        return taskToComplete;
      }
      else if (cancellationToken.IsCancellationRequested)
      {
#if NET_4_5_GREATER
        return Task.FromCanceled<object>(cancellationToken);
#else
        return TaskFromCanceled<object>();
#endif
      }
      else
      {
        return MakeCancellable(taskToComplete, cancellationToken);
      }
    }

    private static async Task MakeCancellable(Task task, CancellationToken cancellationToken)
    {
      var tcs = new TaskCompletionSource<object>();
      using (cancellationToken.Register(() =>
#if NET_4_5_GREATER
                tcs.TrySetCanceled(cancellationToken), useSynchronizationContext: false))
#else
                tcs.TrySetCanceled(), useSynchronizationContext: false))
#endif
      {
#if NET40
        var firstToComplete = await TaskEx.WhenAny(task, tcs.Task).ConfigureAwait(false);
#else
        var firstToComplete = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
#endif

        if (firstToComplete != task)
        {
          task.Ignore();
        }

        await firstToComplete.ConfigureAwait(false);
      }
    }

    /// <summary>For making an uncancellable task cancellable, by ignoring its result.</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="taskToComplete">The task to wait for unless cancelled</param>
    /// <param name="cancellationToken">A cancellation token for cancelling the wait</param>
    /// <param name="message">Message to set in the exception</param>
    /// <returns></returns>
    internal static async Task<T> WithCancellation<T>(this Task<T> taskToComplete, CancellationToken cancellationToken, string message)
    {
      try
      {
        return await taskToComplete.WithCancellation(cancellationToken);
      }
      catch (TaskCanceledException ex)
      {
        throw new TaskCanceledException(message, ex);
      }
    }

    /// <summary>For making an uncancellable task cancellable, by ignoring its result.</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="taskToComplete">The task to wait for unless cancelled</param>
    /// <param name="cancellationToken">A cancellation token for cancelling the wait</param>
    /// <returns></returns>
    internal static Task<T> WithCancellation<T>(this Task<T> taskToComplete, CancellationToken cancellationToken)
    {
      if (taskToComplete.IsCompleted || !cancellationToken.CanBeCanceled)
      {
        return taskToComplete;
      }
      else if (cancellationToken.IsCancellationRequested)
      {
#if NET_4_5_GREATER
        return Task.FromCanceled<T>(cancellationToken);
#else
        return TaskFromCanceled<T>();
#endif
      }
      else
      {
        return MakeCancellable(taskToComplete, cancellationToken);
      }
    }

    private static async Task<T> MakeCancellable<T>(Task<T> task, CancellationToken cancellationToken)
    {
      var tcs = new TaskCompletionSource<T>();
      using (cancellationToken.Register(() =>
#if NET_4_5_GREATER
                tcs.TrySetCanceled(cancellationToken), useSynchronizationContext: false))
#else
                tcs.TrySetCanceled(), useSynchronizationContext: false))
#endif
      {
#if NET40
        var firstToComplete = await TaskEx.WhenAny(task, tcs.Task).ConfigureAwait(false);
#else
        var firstToComplete = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false);
#endif

        if (firstToComplete != task)
        {
          task.Ignore();
        }

        return await firstToComplete.ConfigureAwait(false);
      }
    }

    //    internal static Task WrapInTask(Action action)
    //    {
    //      try
    //      {
    //        action();
    //#if NET_4_5_GREATER
    //        return Task.CompletedTask;
    //#else
    //        return CompletedTask;
    //#endif
    //      }
    //      catch (Exception exc)
    //      {
    //        return Task.FromException<object>(exc);
    //      }
    //    }

    internal static Task<T> ConvertTaskViaTcs<T>(Task<T> task)
    {
#if NET40
      if (task == null) return TaskEx.FromResult(default(T));
#else
      if (task == null) return Task.FromResult(default(T));
#endif

      var resolver = new TaskCompletionSource<T>();

      if (task.Status == TaskStatus.RanToCompletion)
      {
        resolver.TrySetResult(task.Result);
      }
      else if (task.IsFaulted)
      {
        resolver.TrySetException(task.Exception.InnerExceptions);
      }
      else if (task.IsCanceled)
      {
        resolver.TrySetException(new TaskCanceledException(task));
      }
      else
      {
        if (task.Status == TaskStatus.Created) task.Start();

        task.ContinueWith(t =>
        {
          if (t.IsFaulted)
          {
            resolver.TrySetException(t.Exception.InnerExceptions);
          }
          else if (t.IsCanceled)
          {
            resolver.TrySetException(new TaskCanceledException(t));
          }
          else
          {
            resolver.TrySetResult(t.GetResult());
          }
        });
      }
      return resolver.Task;
    }

    //The rationale for GetAwaiter().GetResult() instead of .Result
    //is presented at https://github.com/aspnet/Security/issues/59.      
    internal static T GetResult<T>(this Task<T> task)
    {
      return task.GetAwaiter().GetResult();
    }
    internal static void GetResult(this Task task)
    {
      task.GetAwaiter().GetResult();
    }
  }
}
